using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Xml;
using System.Collections;
using System.IO.Packaging;

using LogAnalyzerIIS.Objects;

namespace LogAnalyzerIIS.LogClasses
{

	/// <summary>
	/// Computes statistics from OpenXML logs
	///</summary>
	public class StatisticsReader : LogConverter
	{
		public StatisticsPool statisticsPool;

		/// <summary>
		///   Initializes new statistics reader.
		/// </summary>
		/// <param name="start">
		///   Start date for statistics.
		/// </param>
		/// <param name="end">
		///   End date for statistics.
		/// </param>
		public StatisticsReader(DateTime start, DateTime end)
		{
			StartDate = start;
			EndDate = end;

			// depending on column type, add corresponding stat object to count statistics
			statisticsPool = new StatisticsPool();
			statisticsPool.StatList.Add(new Dictionary<string, HitsNHosts>()); //0
			statisticsPool.StatList.Add(null);                                 //1
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //2
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //3
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //4
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //5
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //6
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //7
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //8
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //9
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //10
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //11
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //12
			statisticsPool.StatList.Add((Int64)0);                             //13
			statisticsPool.StatList.Add((Int64)0);                             //14
			statisticsPool.StatList.Add((Int64)0);                             //15
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //16
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //17
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //18
			statisticsPool.StatList.Add(new Dictionary<string, int>());        //19
			statisticsPool.StatList.Add((Int64)0);                             //20
			statisticsPool.StatList.Add(new List<string>());                   //21
		}

		/// <summary>
		///   Gets browser and OS name by user-agent string.
		/// </summary>
		/// <param name="UserAgent">
		///   Start date to count statistics.
		/// </param>
		/// <returns>
		///   Returns two strings in an array, where the first string is browser name, the second string is OS name
		/// </returns>
		private string[] GetBrowserAndOS(string userAgentString)
		{
			string[] result = new string[] { "", "" };
			string[] WindowsVersions = { "5.0", "5.1", "5.2", "6.0" };

			List<string> userAgentList = new List<string>();

			// add element into the list (excluding empty strings)
			foreach (string str in userAgentString.Split(new char[] { '+', ')', '(', ';' }))
			{
				if (str != "")
					userAgentList.Add(str);
			}

			int n = 0;

			// try browser identifier
			switch (userAgentList[0])
			{
				case "Mozilla/4.0":
					if ((n = userAgentList.IndexOf("MSIE")) >= 0)
					{
						// replace IE subversion number with 'x'
						try
						{
							result[0] = "Internet Explorer " + userAgentList[n + 1].Split(new char[] { '.' })[0] + ".x";
						}
						catch
						{
							result[0] = "Internet Explorer";
						}
					}

					try
					{
						result[1] = userAgentList[4] + userAgentList[5] + userAgentList[6];
					}
					catch
					{
						result[1] = "";
					}
					break;

				case "Mozilla/5.0":
					result[0] = "Mozilla Firefox";
					try
					{
						if (userAgentList[2] == "Google" && userAgentList[3] == "Desktop")
							result[0] = "Google Desktop";
						else
							result[1] = userAgentList[3] + userAgentList[4] + userAgentList[5];
					}
					catch
					{
						result[1] = "";
					}
					break;

				default:
					result[0] = userAgentList[0];
					break;
			}

			if ((n = userAgentList.IndexOf("Windows")) >= 0)
			{
				foreach (string s in WindowsVersions)
				{
					if (userAgentList.IndexOf(s, n) >= 0)
						result[1] = "WindowsNT" + s;
				}
			}

			return result;
		}

		/// <summary>
		/// Gets extension of file by given URL
		/// </summary>
		/// <param name="url">URL to file where extension should be extracted from</param>
		/// <returns>File extension</returns>
		public static string GetExtensionByURL(string url)
		{
			// split URL into parts before GET parameters and after them
			string[] fullURLSplit = url.Split(new char[] { '?' });

			// split URL before get token using '.'
			string[] uRLSplit = fullURLSplit[0].Split(new char[] { '.' });

			// return last string of the point-splitted array
			if (uRLSplit.Length > 1)
				return uRLSplit[uRLSplit.Length - 1];
			else
				return "";
		}

		/// <summary>
		/// Needed to support class inheritance
		/// </summary>
		protected override XmlDocument CreateWorksheetXML(List<string> sharedStringsOutput, string inputFilePath)
		{
			return null;
		}

		/// <summary>
		/// Performs needed actions when current date of log file changes
		/// </summary>
		/// <param name="currentStat">Daily statistics values</param>
		private void ChangeDate(Statistic currentStat)
		{
			// temporary variable to use for changing value in the dictionary
			HitsNHosts hitsNHosts;

			// if current date changed, flush daily statistics
			if (currentStat.CurrentDate != new DateTime(0))
			{
				if ((statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>).TryGetValue(currentStat.CurrentDate.Date.ToString(), out hitsNHosts))
				{
					hitsNHosts = (statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>)[currentStat.CurrentDate.Date.ToString()];
					hitsNHosts.Hosts = currentStat.HostList.Count;
					(statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>)[currentStat.CurrentDate.Date.ToString()] = hitsNHosts;
				}
				else
					(statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>).Add(currentStat.CurrentDate.Date.ToString(), new HitsNHosts(1, currentStat.HostsCount));

				currentStat.HostsCount += currentStat.HostList.Count;

				// flush entry pages
				foreach (KeyValuePair<string, int> kv in currentStat.DayEntryPages)
				{
					if (statisticsPool.EntryPages.ContainsKey(kv.Key))
						statisticsPool.EntryPages[kv.Key] += kv.Value;
					else
						statisticsPool.EntryPages.Add(kv.Key, kv.Value);
				}

				// flush exit pages
				foreach (KeyValuePair<string, string> kv in currentStat.DayExitIPs)
				{
					if (statisticsPool.ExitPages.ContainsKey(kv.Value))
						statisticsPool.ExitPages[kv.Value]++;
					else
						statisticsPool.ExitPages.Add(kv.Value, 1);
				}

				// flush browsers and OSes
				foreach (HostID host in currentStat.HostList)
				{
					if (statisticsPool.Browsers.ContainsKey(host.Browser))
					{
						hitsNHosts = statisticsPool.Browsers[host.Browser];
						hitsNHosts.Hosts++;
						statisticsPool.Browsers[host.Browser] = hitsNHosts;
					}
					else
						statisticsPool.Browsers.Add(host.Browser, new HitsNHosts(1, 1));

					if (statisticsPool.OSes.ContainsKey(host.OS))
					{
						hitsNHosts = statisticsPool.OSes[host.OS];
						hitsNHosts.Hosts++;
						statisticsPool.OSes[host.OS] = hitsNHosts;
					}
					else
						statisticsPool.OSes.Add(host.OS, new HitsNHosts(1, 1));
				}

				currentStat.HostList.Clear();

				// clear daily lists
				currentStat.DayEntryPages.Clear();
				currentStat.DayEntryIPs.Clear();
				currentStat.DayExitIPs.Clear();
			}

			currentStat.CurrentDate = currentStat.RowDate;
		}

		/// <summary>
		/// Extracts statistics from the log file row
		/// </summary>
		/// <param name="currentStat">Daily statistics values</param>
		private void ProcessRow(Statistic currentStat)
		{
			// load hosts
			HostID host1 = new HostID();

			string[] browserOS = GetBrowserAndOS(currentStat.Row[18]);
			host1.Browser = browserOS[0];
			host1.OS = browserOS[1];
			host1.IP = currentStat.Row[2];

			// if UniqueIPs list doesn't contain current IP, add it to the list
			if (statisticsPool.UniqueIPs.IndexOf(host1.IP) < 0)
				statisticsPool.UniqueIPs.Add(host1.IP);

			// if hosts list doesn't contain current host, add it to the list
			if (currentStat.HostList.IndexOf(host1) < 0)
				currentStat.HostList.Add(host1);

			// temporary variable to use for changing value in the dictionary
			HitsNHosts hitsNHosts;

			// if browsers list contains current browser, increment its hit count,
			// otherwise add browser to the list
			if (statisticsPool.Browsers.ContainsKey(host1.Browser))
			{
				hitsNHosts = statisticsPool.Browsers[host1.Browser];
				hitsNHosts.Hits++;
				statisticsPool.Browsers[host1.Browser] = hitsNHosts;
			}
			else
				statisticsPool.Browsers.Add(host1.Browser, new HitsNHosts(1, 0));

			// if operating systems list contains current browser, increment its hit count,
			// otherwise add browser to the list
			if (statisticsPool.OSes.ContainsKey(host1.OS))
			{
				hitsNHosts = statisticsPool.OSes[host1.OS];
				hitsNHosts.Hits++;
				statisticsPool.OSes[host1.OS] = hitsNHosts;
			}
			else
				statisticsPool.OSes.Add(host1.OS, new HitsNHosts(1, 0));

			// process the URI stem
			// get resource extension
			string s = GetExtensionByURL(currentStat.Row[9]);

			// if resource is page, work with Entry/Exit pages lists
			if (s.Length == 0 || StatisticsWriter.PageExtensions.IndexOf(s) >= 0)
			{
				// if user's IP is in the entry list, it's his first visit, add page to the list
				if (currentStat.DayEntryIPs.IndexOf(currentStat.Row[2]) < 0)
				{
					currentStat.DayEntryIPs.Add(currentStat.Row[2]);

					if (currentStat.DayEntryPages.ContainsKey(currentStat.Row[9]))
						currentStat.DayEntryPages[currentStat.Row[9]]++;
					else
						currentStat.DayEntryPages.Add(currentStat.Row[9], 1);
				}

				// if user's IP is in the exit list, update it with current page URL, 
				// otherwise add page to the list
				if (currentStat.DayExitIPs.ContainsKey(currentStat.Row[2]))
					currentStat.DayExitIPs[currentStat.Row[2]] = currentStat.Row[9];
				else
					currentStat.DayExitIPs.Add(currentStat.Row[2], currentStat.Row[9]);
			}
		}

		/// <summary>
		/// Stores the statistics extracted form the currentStat to the members of the class
		/// </summary>
		/// <param name="currentStat">Daily statistics values</param>
		private void PutStatistics(Statistic currentStat)
		{
			// temporary variable to use for changing value in the dictionary
			HitsNHosts hitsNHosts;

			int n = statisticsPool.StatList.Count > currentStat.Row.Count ? currentStat.Row.Count : statisticsPool.StatList.Count;
			int val = 0;

			for (int i = 1; i < n; i++)
			{
				// if item is dictionary, assume that Row[i] is a key
				if (statisticsPool.StatList[i] is Dictionary<string, int>)
				{
					// increment value of the appropriate key if exists
					if ((statisticsPool.StatList[i] as Dictionary<string, int>).TryGetValue(currentStat.Row[i], out val))
						(statisticsPool.StatList[i] as Dictionary<string, int>)[currentStat.Row[i]]++;
					else
						// or add a new one
						(statisticsPool.StatList[i] as Dictionary<string, int>).Add(currentStat.Row[i], 1);
				}
				else
				{
					// otherwise assume that Row[i] is a value
					if (statisticsPool.StatList[i] is Int64)
					{
						if (int.TryParse(currentStat.Row[i], out val))
							statisticsPool.StatList[i] = (Int64)(statisticsPool.StatList[i]) + val;
					}
				}
			}

			// add or increment hits and hosts number for current day
			// if current date exists in the dictionary, use its values
			if ((statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>).TryGetValue(currentStat.CurrentDate.Date.ToString(), out hitsNHosts))
			{
				hitsNHosts = (statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>)[currentStat.CurrentDate.Date.ToString()];

				hitsNHosts.Hits++;

				hitsNHosts.Hosts = currentStat.HostsCount;

				(statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>)[currentStat.CurrentDate.Date.ToString()] = hitsNHosts;
			}
			else
				// otherwise add new date record to the dictionary
				(statisticsPool.StatList[0] as Dictionary<string, HitsNHosts>).Add(currentStat.CurrentDate.Date.ToString(), new HitsNHosts(1, currentStat.HostsCount));
		}

		/// <summary>
		/// Extracts log date from its file name
		/// </summary>
		/// <param name="filename">Path to file where date should be extracted from</param>
		/// <returns>Date extracted from the filename or current EndDate in case of error during extracting</returns>
		private DateTime ExtractDateFromFilename(string filename)
		{
			try
			{
				// get part of file name where date is stored
				string eventDate = Path.GetFileName(filename).Split(new char[] { '.' })[0].Substring(5);

				// year
				string year = "";
				if (eventDate.Length >= 2)
					year = eventDate.Substring(0, 2);
				int iyear;
				if (!int.TryParse(year, out iyear))
					iyear = StartDate.Year;

				// there are 2 digits for year in the filename, set 50 as bound between 1900x and 2000x
				if (iyear < 100)
				{
					if (iyear < 50)
						iyear += 2000;
					else
						iyear += 1900;
				}

				// month
				string month = "";
				if (eventDate.Length >= 4)
					month = eventDate.Substring(2, 2);
				int imonth;
				if (!int.TryParse(month, out imonth))
					imonth = StartDate.Month;

				string day = "";
				if (eventDate.Length >= 6)
					day = eventDate.Substring(4, 2);
				int iday;
				if (!int.TryParse(day, out iday))
					iday = StartDate.Day;

				return new DateTime(iyear, imonth, iday);
			}
			catch
			{
				// exception - maybe the filename is incorrect, 
				// return value in the reporting period so file will be processed anyway
				return EndDate.Date;
			}
		}

		/// <summary>
		///   Loads log file from the specified path and adds its values into current statistics.
		/// </summary>
		/// <param name="path">
		///   Path to the input file.
		/// </param>
		public string LoadStatisticsFromLogFile(string logFilePath)
		{
			// try to determine log date by its name
			DateTime TestDate = ExtractDateFromFilename(logFilePath);

			// log date isn't in the reporting period, skip processing
			if (TestDate.Date > EndDate.Date || TestDate.Date < StartDate.Date)
				return "Log date is not within reporting period: " + Path.GetFileName(logFilePath);

			// temporary folder path
			try
			{
				Package pack = ZipPackage.Open(logFilePath, FileMode.Open);

				// load shared strings mappings from the sharedStrings.xml
				List<string> sharedStringsSearch = new List<string>();

				using (XmlTextReader reader = new XmlTextReader(pack.GetPart(new Uri("/xl/sharedStrings.xml", UriKind.Relative)).GetStream()))
				{
					for (reader.MoveToContent(); reader.Read(); )
					{
						if (reader.NodeType == XmlNodeType.Element && reader.Name == "si")
						{
							reader.Read();
							sharedStringsSearch.Add(reader.ReadElementString());
						}
					}

					reader.Close();
				}

				// number of row in the log file
				int logRowNumber = 0;

				// number of column in the log file row
				int logColumnNumber = 0;

				Statistic currentStat = new Statistic();

				// reading and processing log worksheet
				using (XmlTextReader reader = new XmlTextReader(pack.GetPart(new Uri("/xl/worksheets/sheet1.xml", UriKind.Relative)).GetStream()))
				{
					for (reader.MoveToContent(); reader.Read(); )
					{
						if (reader.NodeType == XmlNodeType.Element)
						{
							switch (reader.Name)
							{
								case "row":
									// reset column number
									logColumnNumber = 0;

									// if row isn't a header
									if (logRowNumber > 1)
									{
										// get date of current log record
										double dt = double.Parse(currentStat.Row[0]);
										currentStat.RowDate = DateTime.FromOADate(dt).Date;

										// check whether date of the current log record is in reporting period
										if (currentStat.RowDate > EndDate || currentStat.RowDate < StartDate)
											continue;

										// change date boundaries of the log list
										if (currentStat.RowDate < statisticsPool.MinDate)
											statisticsPool.MinDate = currentStat.RowDate;

										if (currentStat.RowDate > statisticsPool.MaxDate)
											statisticsPool.MaxDate = currentStat.RowDate;

										// if date of current row record differs from the date of previous row,
										// store the statistics of previous day
										if (currentStat.CurrentDate != currentStat.RowDate)
											ChangeDate(currentStat);

										// process current row by extracting needed statistics from it
										ProcessRow(currentStat);

										// save extracted statistics into appropriate class members
										PutStatistics(currentStat);
									}

									currentStat.Row.Clear();
									logRowNumber++;
									break;
								// reading next value in a row
								case "c":

									if (logColumnNumber < statisticsPool.StatList.Count && logRowNumber > 1)
									{
										int val = 0;
										string type = reader.GetAttribute("t");

										reader.Read();
										string value = reader.ReadElementString();

										// if value type is string, try to get it from shared strings list
										if (type == "s")
										{
											if (int.TryParse(value, out val))
											{
												if (val < sharedStringsSearch.Count && val >= 0)
													value = sharedStringsSearch[val];
											}
										}

										// add value to the list of the single log record items
										currentStat.Row.Add(value);
									}

									logColumnNumber++;
									break;
							}
						}
					}

					// number of rows in the report excluding header
					statisticsPool.TotalHitsNHosts.Hits += logRowNumber - 1;
					reader.Close();
				}

				#region finalizing

				// as the statistics of the last log file record hasn't been extracted by the code above, do this here
				if (logRowNumber > 1)
				{
					// extract date of the last record
					double dt = double.Parse(currentStat.Row[0]);
					currentStat.RowDate = DateTime.FromOADate(dt).Date;

					// if date of the last record is in the reporting period, process this record
					if (!(currentStat.RowDate > EndDate || currentStat.RowDate < StartDate))
					{
						// change date boundaries in the log list
						if (currentStat.RowDate < statisticsPool.MinDate)
							statisticsPool.MinDate = currentStat.RowDate;

						if (currentStat.RowDate > statisticsPool.MaxDate)
							statisticsPool.MaxDate = currentStat.RowDate;

						// process current row by extracting needed statistics from it
						ProcessRow(currentStat);

						// save extracted statistics into appropriate class members
						PutStatistics(currentStat);
					}

					// force statistics flushing
					currentStat.CurrentDate = currentStat.RowDate;
					ChangeDate(currentStat);

					// increment total hosts nubmer
					statisticsPool.TotalHitsNHosts.Hosts += currentStat.HostsCount;
				}
				#endregion
			}
			catch (Exception ex)
			{
				if (!(ex is System.Threading.ThreadAbortException))
					return "Error: " + ex.Message;
			}
			return "Statistics read complete: " + Path.GetFileName(logFilePath);
		}
	}
}
